package org.apache.ocean.main.search.tag;

import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.ints.IntListIterator;

import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.index.TermPositions;
import org.apache.lucene.store.Directory;
import org.apache.ocean.main.util.SortedLongList;

// TODO: create versioning for tag files
public class TagIndex {
	public static final int MAX_TRANSACTIONS_SIZE = 1024 * 1024 * 25;
	private NavigableMap<Term, TermData> termMap = new TreeMap<Term, TermData>();
	private TagTransactionsFile tagTransactionsFile;
	private Directory directory;
	private ReentrantLock lock = new ReentrantLock();

	public TagIndex(Directory directory, NavigableMap<Term, TermData> termMap) {
		this.directory = directory;
		this.termMap = termMap;
	}

	public void add(TagTransaction tagTransaction) throws IOException {
		lock.lock();
		try {
			tagTransactionsFile.write(tagTransaction);
			if (tagTransaction.add != null) {
				add(tagTransaction.add.term, tagTransaction.add.docs);
			}
			if (tagTransaction.delete != null) {
				add(tagTransaction.delete.term, tagTransaction.delete.docs);
			}
			maybeMerge();
		} finally {
			lock.unlock();
		}
	}

	NavigableMap<Term, TermData> getMap() {
		return termMap;
	}

	private void maybeMerge() throws IOException {
		lock.lock();
		try {
			if (tagTransactionsFile.size() > MAX_TRANSACTIONS_SIZE) {
				TagIndexFile tagIndexFile = new TagIndexFile(directory);
				tagIndexFile.write(this);
			}
		} finally {
			lock.unlock();
		}
	}

	private void add(Term term, int[] docs) {
		TermData termData = termMap.get(term);
		if (termData == null) {
			termData = new TermData(term);
			termMap.put(term, termData);
		}
		for (int doc : docs) {
			termData.docs.add(doc);
		}
	}

	private void delete(Term term, int[] docs) {
		TermData termData = termMap.get(term);
		for (int doc : docs) {
			termData.docs.remove(doc);
		}
	}

	public Collection<String> getFieldNames() {
		Set<String> set = new HashSet<String>(10);
		for (Term term : termMap.keySet()) {
			set.add(term.field());
		}
		return set;
	}

	public Document document(int doc) {
		Document document = new Document();
		for (TermData termData : termMap.values()) {
			if (termData.docs.contains(doc)) {
				Field field = new Field(termData.term.field(), termData.term.text(), Field.Store.YES, Field.Index.UN_TOKENIZED);
				document.add(field);
			}
		}
		return document;
	}

	public TermEnum getTermEnum() {
		return new TagTermEnum();
	}

	public TermEnum getTermEnum(Term term) {
		return new TagTermEnum(term);
	}

	public TermPositions getTermPositions() {
		return new TagTermPositions();
	}

	public class TagTermPositions implements TermPositions {
		private TermData currentTermData;
		private Iterator<TermData> termDataIterator;
		private IntIterator docIterator;
		private int currentDoc = -1;

		public boolean next() throws IOException {
			if (termDataIterator == null) {
				termDataIterator = termMap.values().iterator();
			}
			if (docIterator != null && docIterator.hasNext()) {
				currentDoc = docIterator.nextInt();
				return true;
			}
			if (termDataIterator.hasNext()) {
				currentTermData = termDataIterator.next();
				currentDoc = -1;
				docIterator = currentTermData.docs.iterator();
				return next();
			}
			return false;
		}

		public boolean skipTo(int target) throws IOException {
			// TODO: needs more handling
			// IntRBTreeSet set = (IntRBTreeSet)currentTermData.docs.tailSet(target);
			IntListIterator iterator = currentTermData.docs.tailIterator(target);
			if (!iterator.hasNext())
				return false;
			else {
				docIterator = iterator;
				currentDoc = docIterator.nextInt();
				return true;
			}
		}

		public int nextPosition() {
			return 0;
		}

		public void close() {
		}

		public int doc() {
			return currentDoc;
		}

		public int freq() {
			return 1;
		}

		public int read(int[] docs, int[] freqs) throws IOException {
			int i;
			for (i = 0; i < docs.length; i++) {
				if (!next()) {
					break;
				}
				docs[i] = doc();
				freqs[i] = freq();
			}
			return i;
		}

		public void seek(Term term) throws IOException {
			currentTermData = termMap.get(term);
		}

		public void seek(TermEnum termEnum) throws IOException {
			seek(termEnum.term());
		}

		public byte[] getPayload(byte[] data, int offset) throws IOException {
			throw new UnsupportedOperationException("");
		}

		public int getPayloadLength() {
			throw new UnsupportedOperationException("");
		}

		public boolean isPayloadAvailable() {
			throw new UnsupportedOperationException("");
		}
	}

	public class TagTermEnum extends TermEnum {
		private TermData currentTermData;
		private Iterator<TermData> iterator;

		public TagTermEnum() {
			iterator = termMap.values().iterator();
		}

		public TagTermEnum(Term term) {
			iterator = termMap.tailMap(term).values().iterator();
		}

		public void close() throws IOException {
		}

		public int docFreq() {
			return currentTermData.getCount();
		}

		public boolean next() throws IOException {
			if (iterator.hasNext()) {
				currentTermData = iterator.next();
				return true;
			} else {
				return false;
			}
		}

		public Term term() {
			return currentTermData.getTerm();
		}
	}

	public static class TermData {
		private Term term;
		// TODO: change to sorted array, using binary search
		// private IntRBTreeSet docs = new IntRBTreeSet();
		private SortedLongList docs = new SortedLongList();

		public TermData(Term term, SortedLongList docs) {
			this.term = term;
			this.docs = docs;
		}

		public TermData(Term term) {
			this.term = term;
		}

		public int[] getDocs() {
			return docs.toIntArray();
		}

		public Term getTerm() {
			return term;
		}

		public int getCount() {
			return (int) docs.size();
		}
	}
}
